package com.googlecode.nls4j;

import java.lang.ref.WeakReference;
import java.util.Locale;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * NlsContext is used to associate a {@link Locale} with a particular
 * application and/or user session and to notify {@link LocaleChangeListener}s
 * when the locale has changed.
 * 
 * @author Andrey Pavlenko
 */
public class NlsContext {
    private final Map<LocaleChangeListener, Object> bundles;
    private final Map<LocaleChangeListener, Object> listeners;
    private volatile Locale locale;

    /**
     * Create new NlsContext with {@link Locale#getDefault()} locale.
     */
    public NlsContext() {
        this(Locale.getDefault());
    }

    /**
     * Create new NlsContext with the specified locale.
     * 
     * @param locale
     *            initial Locale.
     * @throws NullPointerException
     *             if the locale is null.
     */
    public NlsContext(final Locale locale) throws NullPointerException {
        if (locale == null) {
            throw new NullPointerException();
        }

        this.locale = locale;
        this.bundles = new WeakHashMap<LocaleChangeListener, Object>();
        this.listeners = new WeakHashMap<LocaleChangeListener, Object>();
    }

    /**
     * Returns the default NlsContext which can be used by multiple applications
     * in the same VM. The initial Locale of this NlsContext is
     * {@link Locale#getDefault()}.
     */
    public static NlsContext getDefault() {
        return DefaultContextHolder.context;
    }

    /**
     * Returns the {@link ThreadLocal} copy of the default NlsContext.
     */
    public static NlsContext getThreadLocal() {
        return ThreadLocalContextHolder.local.get();
    }

    /**
     * Returns the {@link InheritableThreadLocal} copy of the default
     * NlsContext.
     */
    public static NlsContext getInheritableThreadLocal() {
        return InheritableThreadLocalContextHolder.local.get();
    }

    /**
     * Returns the current locale of this NlsContext.
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * Change the current locale of this NlsContext and notify all
     * {@link LocaleChangeListener}s.<br/>
     * <b>Note:</b> the listeners of type {@link CurrentBundle} have the highest
     * priority and will be notified prior to all other listeners.
     * 
     * @param locale
     *            locale to be set.
     * @throws NullPointerException
     *             if the locale is null.
     */
    public synchronized void setLocale(final Locale locale)
            throws NullPointerException {
        final Locale old = this.locale;

        if (!locale.equals(old)) {
            this.locale = locale;

            for (final Map.Entry<LocaleChangeListener, Object> e : bundles
                    .entrySet()) {
                e.getKey().handleLocaleChange(this, old, locale, e.getValue());
            }
            for (final Map.Entry<LocaleChangeListener, Object> e : listeners
                    .entrySet()) {
                e.getKey().handleLocaleChange(this, old, locale, e.getValue());
            }
        }
    }

    /**
     * Register the specified LocaleChangeListener.<br/>
     * <b>Note:</b> this implementation holds {@link WeakReference}s to the
     * listeners which does not prevent the listeners from being garbage
     * collected.
     * 
     * @param listener
     *            a listener to be added.
     * @param handback
     *            an object which will be passed back to the
     *            {@link LocaleChangeListener#handleLocaleChange(NlsContext, Locale, Locale, Object)}
     *            method each time when notifying this listener.
     */
    public synchronized void addLocaleChangeListener(
            final LocaleChangeListener listener, final Object handback) {
        if (listener instanceof CurrentBundle<?, ?>) {
            bundles.put(listener, handback);
        } else {
            listeners.put(listener, handback);
        }
    }

    /**
     * Remove the specified LocaleChangeListener.
     * 
     * @return true if the listener has been found and removed.
     */
    public synchronized boolean removeLocaleChangeListener(
            final LocaleChangeListener listener) {
        if (listener instanceof CurrentBundle<?, ?>) {
            if (bundles.containsKey(listener)) {
                bundles.remove(listener);
                return true;
            }
        } else {
            if (listeners.containsKey(listener)) {
                listeners.remove(listener);
                return true;
            }
        }
        return false;
    }

    private static final class DefaultContextHolder {
        static final NlsContext context = new NlsContext();
    }

    private static final class ThreadLocalContextHolder {
        static final ThreadLocal<NlsContext> local = new ThreadLocal<NlsContext>() {
            @Override
            protected NlsContext initialValue() {
                return new NlsContext();
            }
        };
    }

    private static final class InheritableThreadLocalContextHolder {
        static final InheritableThreadLocal<NlsContext> local = new InheritableThreadLocal<NlsContext>() {
            @Override
            protected NlsContext initialValue() {
                return new NlsContext();
            }
        };
    }
}
